package main

import (
	"context"
	"errors"
	"net/http"
	"os"
	"os/signal"
	"syscall"
	"time"

	"gitea.buaaica.org/ASI/kingbase_exporter/internal/scraper"
	"go.uber.org/zap"
	"go.uber.org/zap/zapcore"
)

// Initialize the configuration used for logger,
// using LoggerOption with all the SetLoggerXxxx methods
// to form a builder pattern to build the configuration.
type loggerOptions struct {
	level string
}

// Logger builder function methods.
type LoggerOption func(*loggerOptions)

// Set logger level.
func SetLoggerLevel(level string) LoggerOption {
	return func(opt *loggerOptions) {
		opt.level = level
	}
}

// Initialize the log, which will replace the global log in zap
// and return a function to clean up the side effects.
func InitLogger(_ context.Context, options ...LoggerOption) (func(), error) {
	o := loggerOptions{}
	for _, v := range options {
		v(&o)
	}

	// setting log level
	atom, err := zap.ParseAtomicLevel(o.level)
	if err != nil {
		return nil, err
	}

	encoderCfg := zap.NewProductionEncoderConfig()
	encoderCfg.TimeKey = ""

	logger := zap.New(zapcore.NewCore(zapcore.NewConsoleEncoder(encoderCfg), zapcore.Lock(os.Stdout), atom))
	undo := zap.ReplaceGlobals(logger)

	return func() {
		undo()
	}, nil
}

// Initialize the configuration used for web,
// using WebOption with all the SetWebXxxx methods
// to form a builder pattern to build the configuration.
type webOptions struct {
	addr string
}

// Web builder function methods.
type WebOption func(*webOptions)

// Set web listen address.
func SetWebAddr(addr string) WebOption {
	return func(wo *webOptions) {
		wo.addr = addr
	}
}

// Start an HTTP service using the injected http.Handler.
// Returns a function that gracefully stops the HTTP service
// without exceeding the timeout when all requests received
// are processed before stopping the service.
func InitWeb(ctx context.Context, handler http.Handler, options ...WebOption) (func(), error) {
	o := webOptions{}
	for _, v := range options {
		v(&o)
	}

	srv := &http.Server{
		Addr:         o.addr,
		Handler:      handler,
		ReadTimeout:  5 * time.Second,
		WriteTimeout: 10 * time.Second,
		IdleTimeout:  15 * time.Second,
	}

	// Start HTTP service in a coroutine process.
	go func() {
		zap.S().Infof("HTTP server is running at %v", o.addr)
		err := srv.ListenAndServe()
		if err != nil && !errors.Is(err, http.ErrServerClosed) {
			panic(err)
		}
	}()

	return func() {
		ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
		defer cancel()

		// Identifies the service as inactive and rejects other requests.
		srv.SetKeepAlivesEnabled(false)
		if err := srv.Shutdown(ctx); err != nil {
			zap.S().Error(err.Error())
		}
	}, nil
}

// This is an initialization function that collocates
// the individual dependencies via dependency injection.
// Returns a cleanup function that will run the cleanup function
// returned by the other initialization functions
// to clean up the side effects.
func Init(ctx context.Context, conf *Conf) (func(), error) {
	cleanLogger, err := InitLogger(ctx, SetLoggerLevel(conf.MustString("log.level")))
	if err != nil {
		return nil, err
	}

	handler := NewRouter(
		ctx,
		SetRouterDsn(conf.MustString(dataSourceKey)),
		SetRouterMetrics(conf.MustString("web.metrics-path")),
		SetRouterTimeout(conf.MustInt("web.timeout")),
		SetRouterScraper(func() (res []scraper.DBScraper) {
			for k := range defaultCollectorFlags {
				if conf.MustBool("collector." + k.Name()) {
					if val, ok := k.(scraper.DBScraper); ok {
						res = append(res, val)
					}
				}
			}
			return
		}()),
	)
	cleanWeb, err := InitWeb(ctx, handler, SetWebAddr(conf.MustString("web.addr")))
	if err != nil {
		return nil, err
	}

	return func() {
		cleanWeb()
		cleanLogger()
	}, nil
}

// The run function intercepts all system stop signals
// and exits the program depending on
// the different system stop signals.
// After exiting the loop, the cleanup function
// is run to clean up the side effects.
// The parsed configuration is passed in then passed to the initialization function.
func Run(ctx context.Context, conf *Conf) error {
	state := 1
	sc := make(chan os.Signal, 1)
	signal.Notify(sc, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
	cleanFunc, err := Init(ctx, conf)
	if err != nil {
		return err
	}

EXIT:
	for {
		sig := <-sc
		switch sig {
		case syscall.SIGQUIT, syscall.SIGTERM, syscall.SIGINT:
			state = 0
			break EXIT
		case syscall.SIGHUP:
		default:
			break EXIT
		}
	}

	cleanFunc()
	time.Sleep(time.Second)
	os.Exit(state)
	return nil
}
